feat: Rust combat engine (38k LOC, 1599 tests, full modifier pipelines)#127
Closed
JackSwitzer wants to merge 22 commits intomainfrom
Closed
feat: Rust combat engine (38k LOC, 1599 tests, full modifier pipelines)#127JackSwitzer wants to merge 22 commits intomainfrom
JackSwitzer wants to merge 22 commits intomainfrom
Conversation
Complete Rust engine (packages/engine-rs/) for MCTS-speed Slay the Spire simulation. Squash of 90 commits from fix/phase4-final. Content: - 746 cards (all 4 characters, base + upgraded) - 66 enemies across Acts 1-4 with full AI - 65 combat relics wired with trigger hooks - 38 potions with real effects - 52 events - 220 status effects on FxHashMap<StatusId, i32> Architecture: - Hook dispatch system (powers/hooks.rs) — static tables, auto-wiring - Typed IDs (StatusId, CardId, RelicId) — Copy newtypes - PyO3 bridge (StSEngine, CombatSolver, RustRunEngine) - 480-dim observation encoding - Full run simulation (map, shop, events, campfire, rewards) Tests: 1691 passing, 0 failing Known gaps (from Codex review): - Necronomicon replay not firing - Guardian mode shift incomplete - EchoForm ignores stack count - ~17 enemies unreachable from encounter pools - No Neow phase in RunEngine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 Opus 4.6 agents audited trigger system, enemy AI, and architecture. Key findings: - 7 missing trigger hook types (on_attacked, on_hp_loss, on_block_gained, etc.) - Time Eater, Transient, Nemesis have broken enemy-specific mechanics - No minion spawning (Collector, Automaton, Reptomancer, GremlinLeader) - ~400 lines dead code (65 dead fns in buffs.rs alone) - Death-check-fairy pattern duplicated 7 times - Block gain scattered 27 places with no central hook - Run is Act 1 only (no Neow, no act transitions) Full details in docs/research/full-audit-2026-04-02.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#129) * feat: add v2 core types — Entity, CardInstance, Intent, Combat Engine v2 foundation. Unified entity model where player and enemies share the same Entity struct. CardInstance is 4 bytes (Copy). Intent is a typed enum replacing scattered move_damage/hits/block fields. Combat struct is the MCTS-friendly snapshot. New file: src/combat_types.rs (alongside existing types, no migration yet) 6 new tests passing (1697 total). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add v2 verb functions — centralized mutation pipeline with reactions 14 verb functions, each applies mutation + fires all relevant reactions: - deal_damage: block, Intangible, Thorns/FlameBarrier retaliation, death - apply_hp_loss: bypass-block damage (poison/burn/constricted) - gain_block: Juggernaut (dmg random enemy), Wave of Hand (Weak all) - apply_debuff/buff: Artifact negation, Curl-Up, Sharp Hide, Malleable, Shifting - draw_cards, exhaust_card, discard_card: pile ops with reaction hooks - heal, gain_energy: capped, tracked Overflow status system maps enemy power IDs (>=64) to reserved slots. 38 new tests (1735 total). All reactions verified by test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: CardRegistry v2 — numeric IDs, O(1) lookup, CardInstance helpers Adds u16 card ID system alongside existing string API: - card_id(&str) -> u16, card_def_by_id(u16) -> &CardDef, card_name(u16) -> &str - make_card/make_card_upgraded for CardInstance construction - is_strike(u16) precomputed bitset for Perfected Strike - Base/upgraded cards get consecutive IDs (Strike_P=N, Strike_P+=N+1) - 741 cards indexed, all existing string methods preserved 10 new tests (1744 total). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: Entity statuses from FxHashMap to [i16; 256] fixed array Direct array indexing eliminates HashMap overhead. 512 bytes per entity, memcpy clone. Removes rustc-hash dependency. All 1744 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: card piles from Vec<String> to Vec<CardInstance> 4-byte Copy struct per card (def_id: u16, cost: i8, flags: u8) replaces heap-allocated strings. O(1) card lookup via def_id index, is_strike() replaces lowercase string search, upgrade_card() replaces string mutation. Added HolyWater card definitions (latent bug from string era). All 1744 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: enemy intents from scattered fields to Intent enum + compact effects Replaces move_damage/move_hits/move_block with typed Intent enum (Copy). Replaces move_effects HashMap<String,i32> with SmallVec<[(u8,i16);4]> using mfx:: constants. Zero heap allocation for enemy moves. Backward-compat methods (move_damage(), set_move()) preserve API surface. All 1744 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: centralized verb pipeline — gain_block, player_lose_hp, hook wiring Centralizes all player block gain through gain_block_player() with Juggernaut and Wave of Hand reactions. Centralizes HP loss through player_lose_hp() with fairy revive, Rupture, and on_hp_loss relics. Wires relics::on_shuffle (Sundial/Abacus), on_enemy_death (Gremlin Horn/Specimen), on_victory (Burning Blood/Black Blood/Meat on Bone). Adds on_enemy_hit reactions (Curl-Up, Malleable, Sharp Hide, Shifting). Deletes dead do_enemy_turns/execute_enemy_move from engine.rs (-157 LOC). All 1744 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: delete 1210 lines of dead code — v2 reference types, combat_verbs, dead powers Removes combat_verbs.rs (893 LOC, superseded by CombatEngine verbs), dead Entity/Combat/StanceV2/EnemyMeta/CombatLine from combat_types.rs, 11 dead functions from buffs.rs and enemy_powers.rs (replaced by hooks dispatch). All 1703 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: wire card replay + 8 missing hooks in play_card/draw_cards Necronomicon replay (2+ cost Attacks), EchoForm stacking fix (first N cards per turn), Unceasing Top (draw on empty hand), Curiosity (enemy Strength on Power play), SkillBurn (damage on Skill play), Forcefield (decrement on card play), Charon's Ashes (damage on exhaust), Evolve (extra draws on Status draw), Fire Breathing (damage on Status/Curse draw). Removes dead replay_pending field. All 1703 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: unified power registry + 4 enemy bug fixes + dead code cleanup Single-source-of-truth PowerRegistryEntry replaces 5-layer hand-wired dispatch (PowerId enum, PowerDef struct, get_power_def(), manual hook tables, scattered install_power match). Net -2577 lines. Bug fixes: Time Eater TIME_WARP_ACTIVE init, Transient FADING init, Nemesis Intangible cycling, boss minion spawning (Collector, Automaton, Reptomancer, GremlinLeader). 9 new integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire all relic/power modifiers into the 6 combat pipelines: - Incoming damage: Paper Crane (Weak 0.60), Odd Mushroom (Vuln 1.25) - Debuff application: Ginger blocks Weak, Turnip blocks Frail - Healing: centralized heal_player() with Mark of Bloom (blocks) + Magic Flower (1.5x) - Card cost: Snecko Eye + Snecko Oil set CONFUSION status - Secondary damage: BowlingBash/Ragnarok use calculate_damage_full (pen nib, flight, etc.) - Potion hooks: Toy Ornithopter heals on potion use Cross-enemy effects via new mfx types (BLOCK_ALL_ALLIES, HEAL_LOWEST_ALLY, STRENGTH_ALL_ALLIES) processed in execute_enemy_move: - Centurion Protect gives block to allies - Mystic heals lowest-HP ally - GremlinLeader Encourage buffs minions Fix unreachable enemy moves: - AcidSlime M/L: add Lick to cycle - WrithingMass: MegaDebuff fires once after first BigHit - Repulsor: 5-turn cycle (Daze x4 -> Attack) Cleanup: fix critical match-arm bug where ~70 passive relics set HAS_MARK_OF_BLOOM, remove dead lookup_by_status, fix stale docstrings. 1599 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Relics implemented: - Symbiotic Virus/Cracked Core/Nuclear Battery: channel orbs at combat start via deferred status flags consumed by engine - Runic Capacitor: +3 orb slots at combat start - Ring of the Serpent: +1 draw per turn (every turn, not just turn 1) - Lizard Tail: revive at 50% HP on death (once per run, after Fairy check) - Slaver's Collar: +3 energy in elite/boss fights (flag-based) - WarpedTongs: upgrade random card in hand at turn start (uses engine RNG) - Strange Spoon: 50% chance exhaust -> shuffle into draw pile - Medical Kit: status cards become playable (exhaust on play, cost 0) - Blue Candle: curse cards become playable (1 HP + exhaust, cost 0) Bug fixes: - Frozen Core now channels Frost (was Lightning) - Potion deal_damage_to_enemy now respects Vulnerable, Intangible, Invincible - Discovery potions (Attack/Skill/Power/Colorless) no longer dead code -- early return removed, proxy card implementations now reachable Cleanup: - Fixed 5 stale docstrings in debuffs.rs - Removed dead lookup_by_status from power registry 1599 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add complete player choice system for MCTS-compatible interactive decisions: ChoiceReason/ChoiceOption/ChoiceContext types, begin_choice() with safety guards, resolve_choice() dispatcher with 9 resolve methods (Scry, DiscardFromHand, ExhaustFromHand, PutOnTopFromHand, PickFromDiscard, PickFromDrawPile, DiscoverCard, PickOption, PlayCardFree). Wire relic counter persistence via CombatState.relic_counters synced to/from RunState.relic_flags at combat boundaries. Add RelicFlags bitfield module with 31 boolean flags + 8 counter indices. Add ~25 card effect handlers including Discovery, Foreign Influence, Omniscience, Wish, Seek, Warcry, Headbutt, exhaust_choose, dual_wield, flame_barrier, calculated_gamble, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ChoiceReason::DualWield with resolve_dual_wield() that duplicates selected hand card (1 copy base, 2 upgraded via max_picks) - Add ChoiceReason::UpgradeCard with resolve_upgrade_card() that sets UPGRADED flag on selected hand card - Add ChoiceReason::PickFromExhaust + ChoiceOption::ExhaustCard with resolve_pick_from_exhaust() that moves card from exhaust to hand - Fix: all three were using PickOption/PickFromDiscard which silently no-oped because resolve_pick_option only matches Named options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Status consumption: - POTION_DRAW: draw cards after potion use (Swift Potion etc.) - CENTENNIAL_PUZZLE_DRAW: draw 3 after unblocked damage - RUNIC_CUBE_DRAW: draw 1 after unblocked damage (persistent) - GREMLIN_HORN_DRAW: draw 1 + gain 1 energy on enemy kill - EMOTION_CHIP_TRIGGER: trigger front orb passive after HP loss Sentry stagger: middle Sentry (index 1) starts on Beam instead of Bolt, matching the real game's alternating pattern. on_victory: call relics::on_victory() after combat win to apply Burning Blood, Black Blood, Meat on the Bone healing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
on_draw (in draw_cards loop): - lose_energy_on_draw: Void loses 1 energy when drawn - copy_on_draw: Endless Agony adds copy to hand when drawn on_discard (new on_card_discarded method): - draw_on_discard: Reflex draws N cards when discarded - energy_on_discard: Tactician gains N energy when discarded - Tracks DISCARDED_THIS_TURN for Sneaky Strike/Eviscerate - Wires Tough Bandages and Tingsha relic triggers - Only fires on manual discard, NOT end-of-turn discard on_retain (in end_turn retain processing): - Establishment: reduce retained card cost - reduce_cost_on_retain: Sands of Time cost reduction - grow_block_on_retain: Perseverance scaling via status counter - grow_damage_on_retain: Windmill Strike scaling via status counter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tag mismatch fixes: - add_shivs: Blade Dance, Cloak and Dagger now fire shiv handler - add_wound_to_draw: Wild Strike adds Wounds to draw pile (not discard) - add_dazed_to_draw: Reckless Charge adds Dazed to draw pile - enlightenment_this_turn / enlightenment_permanent: both variants - apply_lock_on: Bullseye applies Lock-On status New simple effect handlers (25): - HP: lose_hp, lose_hp_gain_energy, lose_hp_gain_str - Draw: draw_to_n, draw_if_no_attacks, draw_if_few_cards_played - Block: block_from_discard, block_if_no_block - Conditionals: if_vulnerable_energy_draw, if_weak_energy_draw, weak_if_attacking, reduce_str_this_turn - Pile: discard_random, shuffle_discard_into_draw, remove_enemy_block - Energy: energy_per_cards_in_draw - Status: no_draw, retain_block (Blur), the_bomb - Card gen: add_wounds_to_hand, poison_random_multi Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add status-counter-based scaling for Rampage (+5/play), Glass Knife (-2/play), Genetic Algorithm (+2 block/play), Ritual Dagger (+3 on kill), and Streamline (reduce cost in piles). Add MCTS-compatible card generation for Infernal Blade, Distraction, Jack of All Trades, Violence, Metamorphosis, Chrysalis, and Transmutation using deterministic representative cards. 6 new tests, 1612 total passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nce overrides Add 9 choice-based card handlers: Secret Weapon/Technique (search draw pile by type), Hologram (return from discard to hand), Forethought (hand to bottom at cost 0), Recycle (exhaust for energy), Concentrate (discard N gain energy), Purity (exhaust from hand), Setup (hand to top at cost 0), Thinking Ahead (draw 2 then put 1 on top). Fix effective_cost_inst/effective_cost_mut_inst to use CardInstance.cost when >= 0, falling back to CardDef.cost only when instance cost is -1 (the default). This ensures runtime cost modifications (Streamline, Madness, Setup, etc.) are properly respected for both playability checks and energy deduction. Also adds FLAG_FREE check. 7 new tests (1613 total passing). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # packages/engine-rs/src/card_effects.rs # packages/engine-rs/src/tests/test_integration.rs
Fix OrbSlots.orbs -> .slots, construct PassiveEffect from orb type for Emotion Chip trigger, add missing relics import in run.rs. 1619 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Power installs: Phantasmal Killer (double damage), Biased Cognition (focus + decay), Amplify, Self Repair (end-of-combat heal), Corpse Explosion (AoE on enemy death), Equilibrium (retain hand), Sentinel (energy on exhaust under Corruption), Escape Plan (conditional block). Dynamic cost: Blood for Blood (-1 per HP lost), Force Field (-1 per active power), Eviscerate (-1 per discard this turn). All wired in both effective_cost_inst and effective_cost_mut_inst. Innate: scan draw pile after shuffle in start_combat, move cards with "innate" tag to top so they appear in opening hand. Misc: Sneaky Strike energy refund, HP_LOSS_THIS_COMBAT tracking, count_active_powers helper, block_if_skill skip in standard block path. 7 new tests (1626 total passing). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…multi-hit Critical fixes: - Draw pile top inverted in 4 resolvers (Scry, Warcry, Headbutt, Setup) used insert(0) instead of push() — put cards on bottom not top - Sharp Hide now uses player_lose_hp() instead of direct hp -= - Double Establishment cost reduction removed from effective_cost - Vigor/Pen Nib only applied on first hit of Bowling Bash and Ragnarok - Gambling Chip restricted to turn 1 only - Runic Cube draw status zeroed after consuming - Beat of Death checks player death between enemy iterations High fixes: - Bouncing Flask+ unified with base handler (base_magic=4 for 4 bounces) - Storm of Steel+ uses same handler, generates Shiv+ via card name check - Necronomicon checks effective cost instead of CardDef base cost - Meditate uses begin_choice for proper card selection from discard - Wish Gold option is now a no-op (can't modify run gold in combat) - Forethought correctly uses insert(0) for bottom of draw pile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nctions Step 0 of modular card effect migration. Adds src/effects/ with: - EffectFlags: 256-bit bitset for O(1) tag checking (replaces O(n) string scan) - CardEffectEntry: static fn pointer table per effect tag (mirrors powers/registry.rs) - 8 dispatch functions: can_play, modify_cost, modify_damage, on_play, on_retain, on_draw, on_discard, post_play_dest - Precomputed hook masks via OnceLock for fast early-out - CardRegistry.effect_flags_vec computed at init time Registry is empty — no behavior change. All 1629 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Stab cost Bug 1: Omniscience now picks from draw pile (PlayCardFreeFromDraw) instead of incorrectly picking from hand. Bug 2: Perseverance grow_block_on_retain and Windmill Strike grow_damage_on_retain bonuses are now read back in damage/block calculations, not just written during end_turn retain hooks. Bug 3: Masterful Stab cost_increase_on_hp_loss handler added to both effective_cost_inst and effective_cost_mut_inst, increasing cost by total_damage_taken (opposite of Blood for Blood). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces O(n) string `contains()` checks with O(1) bitset operations: - can_play: unplayable, only_attack_in_hand, only_attacks_in_hand, only_empty_draw - modify_cost: cost_reduce_on_hp_loss, reduce_cost_per_power, cost_reduce_on_discard, cost_increase_on_hp_loss - on_retain: reduce_cost_on_retain, grow_block_on_retain, grow_damage_on_retain - on_draw: lose_energy_on_draw, copy_on_draw - on_discard: draw_on_discard, energy_on_discard - post_play_dest: shuffle_self_into_draw, end_turn Unifies duplicate cost modification code in effective_cost_inst/effective_cost_mut_inst. All 1629 tests pass with no behavior change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces damage preamble string checks with dispatch_modify_damage: - heavy_blade, damage_equals_block, damage_plus_mantra, perfected_strike - rampage, glass_knife, ritual_dagger, searing_blow - grow_damage_on_retain (Windmill Strike, dual on_retain + modify_damage) - damage_random_x_times (skip generic damage flag) DamageModifier struct merges base_damage_override, base_damage_bonus, strength_multiplier, and skip_generic_damage across all active hooks. Perseverance block bonus migrated to EffectFlags bit check. All 1629 tests pass with no behavior change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Owner
Author
|
Superseded by #131 -- all rust-engine commits are contained in feat/declarative-effects. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full Rust combat engine for MCTS simulation with complete STS combat parity:
heal_player(),gain_block_player(),player_lose_hp(),deal_damage_to_enemy()Test plan
cargo test)🤖 Generated with Claude Code